#include "dicom/session.h"

#include "surbasic/AccessToken.h"
#include "surbasic/surbasic.h"
#include "surbasic/Digestor.h"

#include "dicom/gateway.h"
#include "dicom/upload/cloud_login.h"
#include <gwbase/base/logging.h>
#include <gwbase/base/random.h>

namespace dicom {

std::string GenToken() {
  Random rand;
  static const int kTokenLength = 16;
  static const char kTokenAlphabet[] = {
      'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'G', 'K', 'L', 'M',
      'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
      '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', };

  std::string t;
  for (int i = 0; i < kTokenLength; ++i) {
    t.push_back(kTokenAlphabet[rand.Uniform(sizeof(kTokenAlphabet))]);
  }
  return t;
}

std::string SessionManager::Save(
    const std::string& token, const std::string& surdoc_token,
    const std::string& email, const std::string& password,
    const std::string& ukey_id, const std::string& cloud_root_id,
    const std::string& privkey, int64_t surdoc_key) {
  LockGuard lock(mu_);

  InternalCredentialPtr c(new InternalCredential());
  c->surdoc_token = surdoc_token;
  c->email = email;
  c->password = password;
  c->ukey_id = ukey_id;
  c->cloud_root_id = cloud_root_id;
  c->priv_key = privkey;
  c->surdoc_key = surdoc_key;
  c->update_time = Timestamp::Now();

  tokens_[token] = c;
  LOG_INFO << "updated key: " << tokens_[token]->surdoc_key;
  InsertTokenToDB(token, c);
  return token;
}

CredentialPtr SessionManager::GetUserCredential(const std::string& token) {
  LockGuard lock(mu_);
  LOG_DEBUG << "check token: " << token;
  CredentialMap::iterator it = tokens_.find(token);
  if (it == tokens_.end()) {
    LOG_DEBUG << "invalid token: " << token;
    return CredentialPtr();
  }

  CredentialPtr p(new Credential());
  p->surdoc_key = it->second->surdoc_key;
  p->token = it->second->surdoc_token;
  p->email = it->second->email;
  p->password = it->second->password;
  p->ukey_id = it->second->ukey_id;
  return p;
}

bool SessionManager::GetUserCredentialByEmail(const std::string& user,
                                              SDBasicToken* out) {
  LockGuard lock(mu_);
  CredentialMap::const_iterator it;
  for (it = tokens_.begin(); it != tokens_.end(); ++it) {
    if (it->second->email == user && it->second->surdoc_key > 0) {
      out->email = user;
      out->surdoc_key = it->second->surdoc_key;
      // LOG_INFO << "found key: " << out->surdoc_key;
      return true;
    }
  }
  return false;
}

void SessionManager::Logout(const std::string& token) {
  LockGuard lock(mu_);
  CredentialMap::iterator it = tokens_.find(token);
  if (it == tokens_.end()) return;

  tokens_.erase(token);
  DeleteTokenFromDB(token);
}

void SessionManager::RefreshSingleToken(const std::string& email) {
  Status s;
  // Get all possible passwords
  std::set<string> passwords;
  {
    LockGuard g(mu_);
    CredentialMap::const_iterator it;
    for (it = tokens_.begin(); it != tokens_.end(); ++it) {
      if (it->second->email != email) {
        continue;
      }
      if (passwords.find(it->second->password) != passwords.end()) {
        continue;
      }
      passwords.insert(it->second->password);
    }
  }

  bool ok = false;
  for (std::set<string>::const_iterator pit = passwords.begin();
       pit != passwords.end(); ++pit) {
    AccessToken surdoc_token;
    int64_t surdoc_key;
    std::string root_dir_id;
    std::string server = conf_->GetCloudServer();
    // LOG_INFO << "login to surcloud " << server << ": " << email;
    s = LoginToCloud(server, email, *pit, &surdoc_token, &surdoc_key,
                     &root_dir_id);
    if (s.ok()) {
      // Update all tokens with same email
      LockGuard g(mu_);
      CredentialMap::const_iterator it;
      for (it = tokens_.begin(); it != tokens_.end(); ++it) {
        if (it->second->email != email) {
          continue;
        }
        it->second->surdoc_token = surdoc_token.access_token;
        it->second->surdoc_key = surdoc_key;
        it->second->password = *pit;
        ok = true;
        break;
      }
    }
  }

  if (ok) {
    GetGateway()->sync_mgr()->AddNewUser(email);
    LOG_INFO << "refresh token successfully: " << email;
  }
}

void SessionManager::RefreshTokenLoop() {
  while (true) {
    AccessToken expired;
    if (pool_.GetNextPending(&expired) == false) {
      sleep(5);
      continue;
    }

    RefreshSingleToken(expired.account);
    sleep(5);
  }
}

void SessionManager::StartTokenRefresher() {
  tokenmg::TokenManager::get().SetExpirationHandler(&pool_);
  token_refresher_.reset(new Thread(
      boost::bind(&SessionManager::RefreshTokenLoop, this), "token_refresher"));
  token_refresher_->Start();
}

Status SessionManager::Init() {
  Status s;
  s = OpenTokenDB();
  if (!s.ok()) {
    return s;
  }
  LoadSavedTokenFromDB();
  return s;
}

Status SessionManager::OpenTokenDB() {
  Status s;
  std::string dbpath = path::Join(conf_->GetDbDir(), "token2.db");
  s = db::Database::Open(dbpath, &db_);
  if (!s.ok()) {
    return s;
  }

  const char* sql =
      "create table if not exists token_tab ( "
      "token varchar, "
      "surdoc_token varchar, "
      "email varchar, "
      "password varchar, "
      "ukey_id varchar, "
      "primary key (token))";
  db_->RunSql(sql);
  return s;
}

void SessionManager::LoadSavedTokenFromDB() {
  const char* sql =
      "select token, surdoc_token, email, password, ukey_id "
      "from token_tab";
  LOG_INFO << "SessionManager::LoadSavedTokenFromDB()";
  db::StmtPtr stmt = db_->Prepare(sql);
  stmt->Step();
  while (!stmt->eof()) {
    InternalCredentialPtr c(new InternalCredential());
    int pos = 0;
    std::string token = stmt->GetText(pos++);
    c->surdoc_token = stmt->GetText(pos++);
    c->email = stmt->GetText(pos++);
    c->password = stmt->GetText(pos++);
    c->ukey_id = stmt->GetText(pos++);

    // We'll refresh token when a token with key = -1, see CheckToken()
    c->surdoc_key = -1;
    c->update_time = Timestamp::Now();

    tokens_.insert(std::make_pair(token, c));
    LOG_INFO << "Load token: " << c->email << " : " << c->surdoc_token;
    stmt->Step();
  }
}

void SessionManager::InsertTokenToDB(const std::string& token,
                                     const InternalCredentialPtr& c) {
  const char* sql =
      "replace into token_tab ( "
      "token, "
      "surdoc_token, "
      "email, "
      "password,"
      "ukey_id) "
      "values (?, ?, ?, ?, ?)";
  db::StmtPtr stmt = db_->Prepare(sql);
  stmt->BindText(token);
  stmt->BindText(c->surdoc_token);
  stmt->BindText(c->email);
  stmt->BindText(c->password);
  stmt->BindText(c->ukey_id);
  stmt->Step();
}

void SessionManager::DeleteTokenFromDB(const std::string& token) {
  const char* sql = "delete from token_tab where token = ?";
  db::StmtPtr stmt = db_->Prepare(sql);
  stmt->BindText(token);
  stmt->Step();
}

SessionManager* NewSessionManager(const DicomConf* conf) {
  SessionManager* s = new SessionManager(conf);
  if (!s->Init().ok()) {
    delete s;
    return NULL;
  }
  return s;
}

void ExpiredTokenPool::HandleExpiration(const AccessToken& token) {
  LockGuard g(mu_);
  pending_.push_front(token);
}

bool ExpiredTokenPool::GetNextPending(AccessToken* token) {
  LockGuard g(mu_);
  if (pending_.empty()) {
    return false;
  }

  *token = pending_.front();
  pending_.pop_front();
  return true;
}

size_t ExpiredTokenPool::PendingCount() const {
  LockGuard g(mu_);
  return pending_.size();
}
}
